package logger

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	DEBUG = iota
	INFO
	WARN
	ERROR
)

var LevelMap = map[int][]byte{
	DEBUG: []byte("DEBUG"),
	INFO:  []byte("INFO"),
	WARN:  []byte("WARN"),
	ERROR: []byte("ERROR"),
}

var (
	LeftBracket  = []byte("[")
	RightBracket = []byte("]")
	Space        = []byte(" ")
	Colon        = []byte(":")
	FuncBracket  = []byte("()")
	LineFeed     = []byte("\n")
)

var (
	RED     = []byte{27, 91, 51, 49, 109}
	GREEN   = []byte{27, 91, 51, 50, 109}
	YELLOW  = []byte{27, 91, 51, 51, 109}
	BLUE    = []byte{27, 91, 51, 52, 109}
	MAGENTA = []byte{27, 91, 51, 53, 109}
	CYAN    = []byte{27, 91, 51, 54, 109}
	WHITE   = []byte{27, 91, 51, 55, 109}
	RESET   = []byte{27, 91, 48, 109}
)

var LOG *Logger = nil

var ErrorMsgCallback func(msg string) = nil

const (
	DefaultFileMaxSize = 1024 * 1024 * 10
	LogInfoChanSize    = 1000
	MaxWriteCacheNum   = 1000
)

type Logger struct {
	AppName            string
	Level              int
	TrackLine          bool
	TrackThread        bool
	EnableFile         bool
	FileMaxSize        int32
	DisableColor       bool
	EnableJson         bool
	FilePath           string
	FileSizeCut        bool
	FileTimeCut        bool
	ErrorStack         bool
	File               *os.File
	LastLogTime        time.Time
	LogInfoChan        chan *LogInfo
	WriteBuf           []byte
	WriteCacheNum      int32
	CloseChan          chan struct{}
	GlobalValueMap     map[string]any
	GlobalValueMapLock sync.RWMutex
}

type LogInfo struct {
	Time        time.Time
	Level       int
	Msg         *[]byte
	Raw         bool
	FileName    string
	FuncName    string
	Line        int
	GoroutineId string
	ThreadId    string
}

func ReloadLoggerConfig(logLevel string) {
	LOG.Level = LOG.getLogLevel(logLevel) // 日志级别
}

func InitLogger(appName string, level string) {
	LOG = new(Logger)
	LOG.AppName = appName

	LOG.Level = LOG.getLogLevel(level) // 日志级别
	LOG.TrackLine = true               // 行号追踪
	LOG.TrackThread = false            // 线程追踪
	LOG.EnableFile = true              // 是否启用日志文件
	LOG.FileMaxSize = 0                // 日志文件最大大小
	LOG.DisableColor = false           // 是否禁用日志颜色
	LOG.EnableJson = false             // 是否开启对象json序列化
	LOG.FilePath = "./log"             // 日志文件路径
	LOG.FileSizeCut = false            // 日志文件按大小切分
	LOG.FileTimeCut = true             // 日志文件按时间切分
	LOG.ErrorStack = false             // 错误堆栈打印

	if LOG.FileMaxSize == 0 {
		LOG.FileMaxSize = DefaultFileMaxSize
	}

	if LOG.FileSizeCut && LOG.FileTimeCut {
		panic("???")
	}

	err := os.MkdirAll(LOG.FilePath, 0644)
	if err != nil {
		panic(fmt.Sprintf("make log dir error: %v\n", err))
	}
	_, err = os.Stat(LOG.FilePath + "/" + LOG.AppName + ".log")
	if err != nil && !os.IsNotExist(err) {
		panic(fmt.Sprintf("open old log file error: %v\n", err))
	}
	if err == nil {
		timeStr := time.Now().Format(LogFileNameTimeFormat)
		err = os.Rename(LOG.FilePath+"/"+LOG.AppName+".log", LOG.FilePath+"/"+LOG.AppName+".log"+"."+timeStr+".log")
		if err != nil {
			panic(fmt.Sprintf("rename old log file error: %v\n", err))
		}
	}
	file, err := os.OpenFile(LOG.FilePath+"/"+LOG.AppName+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		panic(fmt.Sprintf("open new log file error: %v\n", err))
	}
	LOG.File = file

	LOG.LastLogTime = time.Now()
	LOG.LogInfoChan = make(chan *LogInfo, LogInfoChanSize)
	LOG.WriteBuf = make([]byte, 0)
	LOG.WriteCacheNum = 0
	LOG.CloseChan = make(chan struct{})
	LOG.GlobalValueMap = make(map[string]any)
	go LOG.doLog()
}

func CloseLogger() {
	LOG.CloseChan <- struct{}{}
	<-LOG.CloseChan
}

func (l *Logger) doLog() {
	var logBuf bytes.Buffer
	timeBuf := make([]byte, 0, 64)
	exit := false
	exitCountDown := 0
	for {
		select {
		case <-l.CloseChan:
			exit = true
			exitCountDown = len(l.LogInfoChan)
		case logInfo := <-l.LogInfoChan:
			var logData []byte = nil
			if !logInfo.Raw {
				if !l.DisableColor {
					logBuf.Write(CYAN)
				}
				logBuf.Write(LeftBracket)
				logBuf.Write(logInfo.Time.AppendFormat(timeBuf, "2006-01-02 15:04:05.000"))
				logBuf.Write(RightBracket)
				if !l.DisableColor {
					logBuf.Write(RESET)
				}
				logBuf.Write(Space)

				if !l.DisableColor {
					switch logInfo.Level {
					case DEBUG:
						logBuf.Write(BLUE)
					case INFO:
						logBuf.Write(GREEN)
					case WARN:
						logBuf.Write(YELLOW)
					case ERROR:
						logBuf.Write(RED)
					}
				}
				logBuf.Write(LeftBracket)
				logBuf.Write(LevelMap[logInfo.Level])
				logBuf.Write(RightBracket)
				if !l.DisableColor {
					logBuf.Write(RESET)
				}
				logBuf.Write(Space)

				if !l.DisableColor && logInfo.Level == ERROR {
					logBuf.Write(RED)
					logBuf.Write(*logInfo.Msg)
					logBuf.Write(RESET)
				} else {
					logBuf.Write(*logInfo.Msg)
				}

				l.GlobalValueMapLock.RLock()
				gvl := len(l.GlobalValueMap)
				if gvl != 0 {
					if !l.DisableColor {
						logBuf.Write(WHITE)
					}
					logBuf.Write(Space)
					logBuf.Write(LeftBracket)
					gvi := 0
					for gvk, gvv := range l.GlobalValueMap {
						logBuf.Write([]byte(fmt.Sprintf("%v:%v", gvk, gvv)))
						gvi++
						if gvi < gvl {
							logBuf.Write(Space)
						}
					}
					logBuf.Write(RightBracket)
					if !l.DisableColor {
						logBuf.Write(RESET)
					}
				}
				l.GlobalValueMapLock.RUnlock()

				if l.TrackLine {
					logBuf.Write(Space)
					if !l.DisableColor {
						logBuf.Write(MAGENTA)
					}
					logBuf.Write(LeftBracket)
					logBuf.Write([]byte(logInfo.FileName))
					logBuf.Write(Colon)
					logBuf.Write([]byte(strconv.Itoa(logInfo.Line)))
					logBuf.Write(Space)
					logBuf.Write([]byte(logInfo.FuncName))
					logBuf.Write(FuncBracket)
					if l.TrackThread {
						logBuf.Write(Space)
						logBuf.Write([]byte("goroutine"))
						logBuf.Write(Colon)
						logBuf.Write([]byte(logInfo.GoroutineId))
						logBuf.Write(Space)
						logBuf.Write([]byte("thread"))
						logBuf.Write(Colon)
						logBuf.Write([]byte(logInfo.ThreadId))
					}
					logBuf.Write(RightBracket)
					if !l.DisableColor {
						logBuf.Write(RESET)
					}
				}

				logBuf.Write(LineFeed)
				logData = logBuf.Bytes()
			} else {
				logData = *logInfo.Msg
				logData = append(logData, LineFeed...)
			}
			l.writeLog(logData, logInfo.Time)
			putBuf(logInfo.Msg)
			logInfoPool.Put(logInfo)
			logBuf.Reset()
			timeBuf = timeBuf[0:0]
			if exit {
				exitCountDown--
			}
		}
		if exit && exitCountDown == 0 {
			LOG.CloseChan <- struct{}{}
			return
		}
	}
}

func (l *Logger) writeLog(logData []byte, logTime time.Time) {
	defer func() {
		l.LastLogTime = logTime
	}()
	if l.FileTimeCut {
		if l.LastLogTime.Day() != logTime.Day() {
			l.cutLogFile()
			l.flushLog()
		}
	}
	l.WriteBuf = append(l.WriteBuf, logData...)
	l.WriteCacheNum++
	if len(l.LogInfoChan) != 0 && l.WriteCacheNum < MaxWriteCacheNum {
		return
	}
	l.flushLog()
}

func (l *Logger) flushLog() {
	l.writeLogConsole(l.WriteBuf)
	if l.EnableFile {
		l.writeLogFile(l.WriteBuf)
	}
	l.WriteBuf = l.WriteBuf[0:0]
	l.WriteCacheNum = 0
}

func (l *Logger) writeLogConsole(logData []byte) {
	_, _ = os.Stderr.Write(logData)
}

func (l *Logger) writeLogFile(logData []byte) {
	if l.FileSizeCut {
		fileStat, err := l.File.Stat()
		if err != nil {
			_, _ = os.Stderr.WriteString(fmt.Sprintf(string(RED)+"get log file stat error: %v\n"+string(RESET), err))
			return
		}
		if fileStat.Size() >= int64(l.FileMaxSize) {
			l.cutLogFile()
		}
	}
	_, err := l.File.Write(logData)
	if err != nil {
		_, _ = os.Stderr.WriteString(fmt.Sprintf(string(RED)+"write log file error: %v\n"+string(RESET), err))
		return
	}
}

func (l *Logger) cutLogFile() {
	err := l.File.Close()
	if err != nil {
		_, _ = os.Stderr.WriteString(fmt.Sprintf(string(RED)+"close old log file error: %v\n"+string(RESET), err))
		return
	}
	timeStr := l.LastLogTime.Format(LogFileNameTimeFormat)
	err = os.Rename(l.File.Name(), l.File.Name()+"."+timeStr+".log")
	if err != nil {
		_, _ = os.Stderr.WriteString(fmt.Sprintf(string(RED)+"rename old log file error: %v\n"+string(RESET), err))
		return
	}
	file, err := os.OpenFile(l.FilePath+"/"+l.AppName+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		_, _ = os.Stderr.WriteString(fmt.Sprintf(string(RED)+"open new log file error: %v\n"+string(RESET), err))
		return
	}
	l.File = file
}

var bufPool = sync.Pool{New: func() any { return new([]byte) }}

func getBuf() *[]byte {
	p := bufPool.Get().(*[]byte)
	*p = (*p)[0:0]
	return p
}

func putBuf(p *[]byte) {
	if cap(*p) > 64<<10 {
		*p = nil
	}
	bufPool.Put(p)
}

var logInfoPool = sync.Pool{New: func() any { return new(LogInfo) }}

func Debug(msg string, param ...any) {
	if LOG.Level > DEBUG {
		return
	}
	logInfo := logInfoPool.Get().(*LogInfo)
	logInfo.Time = time.Now()
	logInfo.Level = DEBUG
	logInfo.Raw = false
	buf := getBuf()
	if LOG.EnableJson {
		jsonList := make([]any, 0, len(param))
		for _, obj := range param {
			data, _ := json.Marshal(obj)
			jsonList = append(jsonList, string(data))
		}
		param = jsonList
	}
	*buf = fmt.Appendf(*buf, msg, param...)
	logInfo.Msg = buf
	if LOG.TrackLine {
		logInfo.FileName, logInfo.Line, logInfo.FuncName = LOG.getLineFunc()
	}
	if LOG.TrackThread {
		logInfo.GoroutineId = LOG.getGoroutineId()
		logInfo.ThreadId = LOG.getThreadId()
	}
	LOG.LogInfoChan <- logInfo
}

func Info(msg string, param ...any) {
	if LOG.Level > INFO {
		return
	}
	logInfo := logInfoPool.Get().(*LogInfo)
	logInfo.Time = time.Now()
	logInfo.Level = INFO
	logInfo.Raw = false
	buf := getBuf()
	if LOG.EnableJson {
		jsonList := make([]any, 0, len(param))
		for _, obj := range param {
			data, _ := json.Marshal(obj)
			jsonList = append(jsonList, string(data))
		}
		param = jsonList
	}
	*buf = fmt.Appendf(*buf, msg, param...)
	logInfo.Msg = buf
	if LOG.TrackLine {
		logInfo.FileName, logInfo.Line, logInfo.FuncName = LOG.getLineFunc()
	}
	if LOG.TrackThread {
		logInfo.GoroutineId = LOG.getGoroutineId()
		logInfo.ThreadId = LOG.getThreadId()
	}
	LOG.LogInfoChan <- logInfo
}

func Warn(msg string, param ...any) {
	if LOG.Level > WARN {
		return
	}
	logInfo := logInfoPool.Get().(*LogInfo)
	logInfo.Time = time.Now()
	logInfo.Level = WARN
	logInfo.Raw = false
	buf := getBuf()
	if LOG.EnableJson {
		jsonList := make([]any, 0, len(param))
		for _, obj := range param {
			data, _ := json.Marshal(obj)
			jsonList = append(jsonList, string(data))
		}
		param = jsonList
	}
	*buf = fmt.Appendf(*buf, msg, param...)
	logInfo.Msg = buf
	if LOG.TrackLine {
		logInfo.FileName, logInfo.Line, logInfo.FuncName = LOG.getLineFunc()
	}
	if LOG.TrackThread {
		logInfo.GoroutineId = LOG.getGoroutineId()
		logInfo.ThreadId = LOG.getThreadId()
	}
	LOG.LogInfoChan <- logInfo
}

func Error(msg string, param ...any) {
	if LOG.Level > ERROR {
		return
	}
	logInfo := logInfoPool.Get().(*LogInfo)
	logInfo.Time = time.Now()
	logInfo.Level = ERROR
	logInfo.Raw = false
	buf := getBuf()
	if LOG.EnableJson {
		jsonList := make([]any, 0, len(param))
		for _, obj := range param {
			data, _ := json.Marshal(obj)
			jsonList = append(jsonList, string(data))
		}
		param = jsonList
	}
	if LOG.ErrorStack {
		msg += "\n%v"
		param = append(param, Stack())
	}
	*buf = fmt.Appendf(*buf, msg, param...)
	logInfo.Msg = buf
	if LOG.TrackLine {
		logInfo.FileName, logInfo.Line, logInfo.FuncName = LOG.getLineFunc()
	}
	if LOG.TrackThread {
		logInfo.GoroutineId = LOG.getGoroutineId()
		logInfo.ThreadId = LOG.getThreadId()
	}
	LOG.LogInfoChan <- logInfo
	if ErrorMsgCallback != nil {
		ErrorMsgCallback(string(*buf))
	}
}

func Raw(data []byte) {
	logInfo := logInfoPool.Get().(*LogInfo)
	logInfo.Time = time.Now()
	logInfo.Raw = true
	buf := getBuf()
	*buf = append(*buf, data...)
	logInfo.Msg = buf
	LOG.LogInfoChan <- logInfo
}

func (l *Logger) getLogLevel(level string) int {
	switch strings.ToUpper(level) {
	case "DEBUG":
		return DEBUG
	case "INFO":
		return INFO
	case "WARN":
		return WARN
	case "ERROR":
		return ERROR
	default:
		panic(fmt.Sprintf("unknown log level: %v", level))
	}
}

func (l *Logger) getGoroutineId() (goroutineId string) {
	buf := make([]byte, 32)
	runtime.Stack(buf, false)
	buf = bytes.TrimPrefix(buf, []byte("goroutine "))
	buf = buf[:bytes.IndexByte(buf, ' ')]
	goroutineId = string(buf)
	return goroutineId
}

func (l *Logger) getLineFunc() (fileName string, line int, funcName string) {
	var pc uintptr
	var file string
	var ok bool
	pc, file, line, ok = runtime.Caller(2)
	if !ok {
		return "???", 0, "???"
	}
	fileName = path.Base(file)
	funcName = runtime.FuncForPC(pc).Name()
	for i := len(funcName) - 1; i >= 0; i-- {
		if funcName[i] == '.' {
			funcName = funcName[i+1:]
			break
		}
	}
	return fileName, line, funcName
}

func Stack() string {
	buf := make([]byte, 1024)
	for {
		n := runtime.Stack(buf, false)
		if n < len(buf) {
			return string(buf[:n])
		}
		buf = make([]byte, 2*len(buf))
	}
}

func StackAll() string {
	buf := make([]byte, 1024*16)
	for {
		n := runtime.Stack(buf, true)
		if n < len(buf) {
			return string(buf[:n])
		}
		buf = make([]byte, 2*len(buf))
	}
}

func SetGlobalValue(key string, value any) {
	LOG.GlobalValueMapLock.Lock()
	LOG.GlobalValueMap[key] = value
	LOG.GlobalValueMapLock.Unlock()
}

func ClearGlobalValue(key string) {
	LOG.GlobalValueMapLock.Lock()
	delete(LOG.GlobalValueMap, key)
	LOG.GlobalValueMapLock.Unlock()
}

type LogWriter struct {
}

func (l *LogWriter) Write(p []byte) (n int, err error) {
	Raw(p)
	return len(p), nil
}
